﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Mono.Cecil;

namespace Mono.Linker
{
    // Copied from https://github.com/jbevain/cecil/blob/master/Mono.Cecil/MethodReferenceComparer.cs
    internal sealed class MethodReferenceComparer : EqualityComparer<MethodReference>
    {
        // Initialized lazily for each thread
        [ThreadStatic]
        static List<MethodReference>? xComparisonStack;

        [ThreadStatic]
        static List<MethodReference>? yComparisonStack;

        public readonly ITryResolveMetadata _resolver;

        public MethodReferenceComparer(ITryResolveMetadata resolver)
        {
            _resolver = resolver;
        }

        public override bool Equals(MethodReference? x, MethodReference? y)
        {
            return AreEqual(x, y, _resolver);
        }

        public override int GetHashCode(MethodReference obj)
        {
            return GetHashCodeFor(obj);
        }

        public static bool AreEqual(MethodReference? x, MethodReference? y, ITryResolveMetadata resolver)
        {
            if (ReferenceEquals(x, y))
                return true;

            if (x is null ^ y is null)
                return false;

            Debug.Assert(x is not null);
            Debug.Assert(y is not null);

            if (x.HasThis != y.HasThis)
                return false;

#pragma warning disable RS0030 // MethodReference.HasParameters is banned - this code is copied from Cecil
            if (x.HasParameters != y.HasParameters)
                return false;
#pragma warning restore RS0030

            if (x.HasGenericParameters != y.HasGenericParameters)
                return false;

#pragma warning disable RS0030 // MethodReference.HasParameters is banned - this code is copied from Cecil
            if (x.Parameters.Count != y.Parameters.Count)
                return false;
#pragma warning restore RS0030

            if (x.Name != y.Name)
                return false;

            if (!TypeReferenceEqualityComparer.AreEqual(x.DeclaringType, y.DeclaringType, resolver))
                return false;

            var xGeneric = x as GenericInstanceMethod;
            var yGeneric = y as GenericInstanceMethod;
            if (xGeneric != null || yGeneric != null)
            {
                if (xGeneric == null || yGeneric == null)
                    return false;

                if (xGeneric.GenericArguments.Count != yGeneric.GenericArguments.Count)
                    return false;

                for (int i = 0; i < xGeneric.GenericArguments.Count; i++)
                    if (!TypeReferenceEqualityComparer.AreEqual(xGeneric.GenericArguments[i], yGeneric.GenericArguments[i], resolver))
                        return false;
            }

            var xResolved = resolver.TryResolve(x);
            var yResolved = resolver.TryResolve(y);

            if (xResolved != yResolved)
                return false;

            if (xResolved == null)
            {
                // We couldn't resolve either method. In order for them to be equal, their parameter types _must_ match. But wait, there's a twist!
                // There exists a situation where we might get into a recursive state: parameter type comparison might lead to comparing the same
                // methods again if the parameter types are generic parameters whose owners are these methods. We guard against these by using a
                // thread static list of all our comparisons carried out in the stack so far, and if we're in progress of comparing them already,
                // we'll just say that they match.

                xComparisonStack ??= new List<MethodReference>();

                yComparisonStack ??= new List<MethodReference>();

                for (int i = 0; i < xComparisonStack.Count; i++)
                {
                    if (xComparisonStack[i] == x && yComparisonStack[i] == y)
                        return true;
                }

                xComparisonStack.Add(x);

                try
                {
                    yComparisonStack.Add(y);

                    try
                    {
#pragma warning disable RS0030 // MethodReference.HasParameters is banned - this code is copied from Cecil
                        for (int i = 0; i < x.Parameters.Count; i++)
                        {
                            if (!TypeReferenceEqualityComparer.AreEqual(x.Parameters[i].ParameterType, y.Parameters[i].ParameterType, resolver))
                                return false;
                        }
#pragma warning restore RS0030
                    }
                    finally
                    {
                        yComparisonStack.RemoveAt(yComparisonStack.Count - 1);
                    }
                }
                finally
                {
                    xComparisonStack.RemoveAt(xComparisonStack.Count - 1);
                }
            }

            return true;
        }

        public static bool AreSignaturesEqual(MethodReference x, MethodReference y, ITryResolveMetadata resolver, TypeComparisonMode comparisonMode = TypeComparisonMode.Exact)
        {
            if (x.HasThis != y.HasThis)
                return false;

#pragma warning disable RS0030 // MethodReference.HasParameters is banned - this code is copied from Cecil
            if (x.Parameters.Count != y.Parameters.Count)
                return false;
#pragma warning restore RS0030

            if (x.GenericParameters.Count != y.GenericParameters.Count)
                return false;

#pragma warning disable RS0030 // MethodReference.HasParameters is banned - this code is copied from Cecil
            for (var i = 0; i < x.Parameters.Count; i++)
                if (!TypeReferenceEqualityComparer.AreEqual(x.Parameters[i].ParameterType, y.Parameters[i].ParameterType, resolver, comparisonMode))
                    return false;
#pragma warning restore RS0030

            if (!TypeReferenceEqualityComparer.AreEqual(x.ReturnType, y.ReturnType, resolver, comparisonMode))
                return false;

            return true;
        }

        public static int GetHashCodeFor(MethodReference obj)
        {
            // a very good prime number
            const int hashCodeMultiplier = 486187739;

            var genericInstanceMethod = obj as GenericInstanceMethod;
            if (genericInstanceMethod != null)
            {
                var hashCode = GetHashCodeFor(genericInstanceMethod.ElementMethod);
                for (var i = 0; i < genericInstanceMethod.GenericArguments.Count; i++)
                    hashCode = hashCode * hashCodeMultiplier + TypeReferenceEqualityComparer.GetHashCodeFor(genericInstanceMethod.GenericArguments[i]);
                return hashCode;
            }

            return TypeReferenceEqualityComparer.GetHashCodeFor(obj.DeclaringType) * hashCodeMultiplier + obj.Name.GetHashCode();
        }
    }
}
